<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=935506
-->
<head>
  <title>Test key events for number control</title>
  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
  <meta charset="UTF-8">
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=935506">Mozilla Bug 935506</a>
<p id="display"></p>
<div id="content">
  <input id="input" type="number">
</div>
<pre id="test">
<script type="application/javascript">

/**
 * Test for Bug 935506
 * This test checks how the value of <input type=number> changes in response to
 * key events while it is in various states.
 **/
SimpleTest.waitForExplicitFinish();
// Turn off Spatial Navigation because it hijacks arrow keydown events:
SimpleTest.waitForFocus(function() {
  SpecialPowers.pushPrefEnv({"set":[["snav.enabled", false]]}, function() {
    test();
    SimpleTest.finish();
  });
});
const defaultMinimum = "NaN";
const defaultMaximum = "NaN";
const defaultStep = 1;

// Helpers:
// For the sake of simplicity, we do not currently support fractional value,
// step, etc.

function getMinimum(element) {
  return Number(element.min || defaultMinimum);
}

function getMaximum(element) {
  return Number(element.max || defaultMaximum);
}

function getDefaultValue(element) {
  return 0;
}

function getValue(element) {
  return Number(element.value || getDefaultValue(element));
}

function getStep(element) {
  if (element.step == "any") {
    return "any";
  }
  var step = Number(element.step || defaultStep);
  return step <= 0 ? defaultStep : step;
}

function getStepBase(element) {
  return Number(element.getAttribute("min") || "NaN") ||
         Number(element.getAttribute("value") || "NaN") || 0;
}

function hasStepMismatch(element) {
  var value = element.value;
  if (value == "") {
    value = 0;
  }
  var step = getStep(element);
  if (step == "any") {
    return false;
  }
  return ((value - getStepBase(element)) % step) != 0;
}

function floorModulo(x, y) {
  return (x - y * Math.floor(x / y));
}

function expectedValueAfterStepUpOrDown(stepFactor, element) {
  var value = getValue(element);
  if (isNaN(value)) {
    value = 0;
  }
  var step = getStep(element);
  if (step == "any") {
    step = 1;
  }

  var minimum = getMinimum(element);
  var maximum = getMaximum(element);
  if (!isNaN(maximum)) {
    // "max - (max - stepBase) % step" is the nearest valid value to max.
    maximum = maximum - floorModulo(maximum - getStepBase(element), step);
  }

  // Cases where we are clearly going in the wrong way.
  // We don't use ValidityState because we can be higher than the maximal
  // allowed value and still not suffer from range overflow in the case of
  // of the value specified in @max isn't in the step.
  if ((value <= minimum && stepFactor < 0) ||
      (value >= maximum && stepFactor > 0)) {
    return value;
  }

  if (hasStepMismatch(element) &&
      value != minimum && value != maximum) {
    if (stepFactor > 0) {
      value -= floorModulo(value - getStepBase(element), step);
    } else if (stepFactor < 0) {
      value -= floorModulo(value - getStepBase(element), step);
      value += step;
    }
  }

  value += step * stepFactor;

  // When stepUp() is called and the value is below minimum, we should clamp on
  // minimum unless stepUp() moves us higher than minimum.
  if (element.validity.rangeUnderflow && stepFactor > 0 &&
      value <= minimum) {
    value = minimum;
  } else if (element.validity.rangeOverflow && stepFactor < 0 &&
             value >= maximum) {
    value = maximum;
  } else if (stepFactor < 0 && !isNaN(minimum)) {
    value = Math.max(value, minimum);
  } else if (stepFactor > 0 && !isNaN(maximum)) {
    value = Math.min(value, maximum);
  }

  return value;
}

function expectedValAfterKeyEvent(key, element) {
  return expectedValueAfterStepUpOrDown(key == "VK_UP" ? 1 : -1, element);
}

function test() {
  var elem = document.getElementById("input");
  elem.focus();

  elem.min = -5;
  elem.max = 5;
  elem.step = 2;
  var defaultValue = 0;
  var oldVal, expectedVal;

  for (key of ["VK_UP", "VK_DOWN"]) {
    // Start at middle:
    oldVal = elem.value = -1;
    expectedVal = expectedValAfterKeyEvent(key, elem);
    synthesizeKey(key, {});
    is(elem.value, String(expectedVal), "Test " + key + " for number control with value set between min/max (" + oldVal + ")");

    // Same again:
    expectedVal = expectedValAfterKeyEvent(key, elem);
    synthesizeKey(key, {});
    is(elem.value, String(expectedVal), "Test repeat of " + key + " for number control");

    // Start at maximum:
    oldVal = elem.value = elem.max;
    expectedVal = expectedValAfterKeyEvent(key, elem);
    synthesizeKey(key, {});
    is(elem.value, String(expectedVal), "Test " + key + " for number control with value set to the maximum (" + oldVal + ")");

    // Same again:
    expectedVal = expectedValAfterKeyEvent(key, elem);
    synthesizeKey(key, {});
    is(elem.value, String(expectedVal), "Test repeat of " + key + " for number control");

    // Start at minimum:
    oldVal = elem.value = elem.min;
    expectedVal = expectedValAfterKeyEvent(key, elem);
    synthesizeKey(key, {});
    is(elem.value, String(expectedVal), "Test " + key + " for number control with value set to the minimum (" + oldVal + ")");

    // Same again:
    expectedVal = expectedValAfterKeyEvent(key, elem);
    synthesizeKey(key, {});
    is(elem.value, String(expectedVal), "Test repeat of " + key + " for number control");

    // Test preventDefault():
    elem.addEventListener("keypress", function(evt) {
      evt.preventDefault();
      elem.removeEventListener("keypress", arguments.callee, false);
    }, false);
    oldVal = elem.value = 0;
    expectedVal = 0;
    synthesizeKey(key, {});
    is(elem.value, String(expectedVal), "Test " + key + " for number control where scripted preventDefault() should prevent the value changing");

    // Test step="any" behavior:
    var oldStep = elem.step;
    elem.step = "any";
    oldVal = elem.value = 0;
    expectedVal = expectedValAfterKeyEvent(key, elem);
    synthesizeKey(key, {});
    is(elem.value, String(expectedVal), "Test " + key + " for number control with value set to the midpoint and step='any' (" + oldVal + ")");
    elem.step = oldStep; // restore

    // Test that invalid input blocks UI initiated stepping:
    oldVal = elem.value = "";
    elem.select();
    sendString("abc");
    synthesizeKey(key, {});
    is(elem.value, "", "Test " + key + " does nothing when the input is invalid");

    // Test that no value does not block UI initiated stepping:
    oldVal = elem.value = "";
    elem.setAttribute("required", "required");
    elem.select();
    expectedVal = expectedValAfterKeyEvent(key, elem);
    synthesizeKey(key, {});
    is(elem.value, String(expectedVal), "Test " + key + " for number control with value set to the empty string and with the 'required' attribute set");

    // Same again:
    expectedVal = expectedValAfterKeyEvent(key, elem);
    synthesizeKey(key, {});
    is(elem.value, String(expectedVal), "Test repeat of " + key + " for number control");

    // Reset 'required' attribute:
    elem.removeAttribute("required");
  }

  // Test that key events are correctly dispatched
  elem.max = "";
  elem.value = "";
  sendString("7837281");
  is(elem.value, "7837281", "Test keypress event dispatch for number control");
}

</script>
</pre>
</body>
</html>
